Import Library¶

In [1]:
import pandas as pd
from joblib import dump, load
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, classification_report
from imblearn.over_sampling import RandomOverSampler
import plotly.express as px
import plotly.graph_objs as go
import plotly.offline as pyo

pyo.init_notebook_mode()

Preprocessing¶

Sebelum masuk pada tahap modelling, kita perlu melakukan preprocessing terlebih dahulu untuk mempersiapkan data yang akan digunakan.

In [2]:
# Load joblib file
df = load('joblib/df.joblib')
df_predict = load('joblib/df_pred.joblib')
In [3]:
# Split label and features
X = df.drop('Attrition', axis=1)
y = df['Attrition']
In [4]:
# Visualisasi Label
count = y.map({0: 'False', 1: 'True'}).value_counts()
fig = px.bar(count, x=count.index, y=count.values,
             title='Attrition', color=count.index)
fig.update_layout(height=400, width=500)
fig.show()
  • Jumlah data Attrition dengan nilai 0(False) bejumlah sangat rendah dibandingkan dengan nilai 1(True). Hal ini disebut dengan imbalanced data. Untuk mengatasi hal ini, kita dapat melakukan oversampling pada data Attrition dengan nilai 1(True) agar jumlahnya sama dengan data Attrition dengan nilai 0(False). Jika tidak dilakukan, maka model yang dibuat akan cenderung memprediksi nilai 0(False) karena jumlahnya yang lebih banyak.

Oversampling¶

In [5]:
# Oversampling
oversample = RandomOverSampler(sampling_strategy='minority', random_state=23)
X, y = oversample.fit_resample(X, y)

# Pasca Oversampling
count = y.map({0: 'False', 1: 'True'}).value_counts()
fig = px.bar(count, x=count.index, y=count.values,
             color=count.index, title='Attrition')
fig.update_layout(height=400, width=500)
fig.show()
In [6]:
# Memisahkan Training dan Testing
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=23)
print('Shape of X_train:', X_train.shape)
print('Shape of X_test:', X_test.shape)
Shape of X_train: (1230, 30)
Shape of X_test: (528, 30)

Dalam contoh ini perbandingan data training dan testing adalah 70:30. Coba ubah parameter test_size menjadi nilai lain dan lihat perbedaan hasil modelnya.

Note: Inisiasi nilai random_state bertujuan untuk menghasilkan nilai yang sama setiap kali kita melakukan proses training dan testing. Jika tidak dilakukan, maka nilai yang dihasilkan akan berbeda setiap kali kita melakukan proses training dan testing. Inisiasi ini dilakukan untuk memastikan bahwa hasil model yang kita buat dapat direproduksi.

Encoding & Standardization¶

Pada tahap analisis telah diketahui bahwa terdapat beberapa kolom kategorikal. Agar data dapat diproses oleh model, maka kita perlu melakukan encoding terhadap kolom kategorikal tersebut dengan mengubah data menjadi numerik. Selain itu, kita juga perlu melakukan standardisasi terhadap data numerik agar data yang memiliki skala yang berbeda dapat diproses dengan baik oleh model.

In [7]:
# Menentukan variabel numerik dan kategorikal
num_var = df.select_dtypes(include=['number']).columns[1:].tolist()
cat_var = df.select_dtypes(include=['object']).columns.tolist()

# Preprocessor
preprocessor = ColumnTransformer(
    transformers=[
        ('numeric', StandardScaler(), num_var),
        ('category', OneHotEncoder(drop='first'), cat_var)
    ]
)

# Fit dan transform training data
X_train_preprocessed = preprocessor.fit_transform(X_train)

# Transform testing data
X_test_preprocessed = preprocessor.transform(X_test)

# Mengambil nama kolom dari variabel one-hot encoded
cat_var_ohe = preprocessor.named_transformers_[
    'category'].get_feature_names_out(cat_var)
cat_var_ohe = cat_var_ohe.tolist()

# Menggabungkan nama kolom numerik dan kategorikal
feature_names = num_var + cat_var_ohe
  • Agar data dapat diolah oleh model, maka data perlu diubah ke dalam bentuk numerik. Hal ini dilakukan dengan menggunakan OneHotEncoder.
  • Pengambilan nama kolom pada data yang telah di-encoding dilakukan untuk memvisualisasikan data pada tahap feature importance.

Machine Learning¶

Algoritma yang digunakan dalam project ini adalah Random Forest Classifier dan logistic regression. Kedua algoritma ini merupakan algoritma yang digunakan untuk melakukan klasifikasi. Kedua algoritma ini memiliki kelebihan dan kekurangan masing-masing. Untuk mengetahui algoritma mana yang lebih baik, kita perlu membandingkan hasil dari kedua algoritma tersebut.

In [8]:
lr_clf = LogisticRegression()
rf_clf = RandomForestClassifier()
In [9]:
# Daftar parameter untuk Logistic Regression
lr_param = {
    'C': [0.1, 1, 3, 5],
    'max_iter': [100, 150, 200, 250],
    'solver': ['newton-cg', 'lbfgs', 'liblinear', 'sag', 'saga'],
}

# Daftar parameter untuk Random Forest
rf_param = {
    'n_estimators': [100, 500, 1000],
    'max_depth': [10, 20, 30, 50],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4],
}

GridSearchCV digunakan untuk mencari kombinasi parameter terbaik dari kedua algoritma tersebut. Parameter terbaik yang ditemukan akan digunakan untuk membuat model akhir.

Coba ubah nilai daftar parameter dan lihat perbedaan hasil modelnya.

In [10]:
# Grid Search

lr_random_search = GridSearchCV(lr_clf, lr_param, n_jobs=-1, verbose=1, scoring='accuracy')
rf_random_search = GridSearchCV(rf_clf, rf_param, n_jobs=-1, verbose=1, scoring='accuracy')
In [11]:
# Fitting model
lr_model = lr_random_search.fit(X_train_preprocessed, y_train)
rf_model = rf_random_search.fit(X_train_preprocessed, y_train)
Fitting 5 folds for each of 80 candidates, totalling 400 fits
Fitting 5 folds for each of 108 candidates, totalling 540 fits
In [12]:
# Menampilkan score kombinasi parameter terbaik dan parameternya
lr_best_params = lr_random_search.best_params_
lr_best_score = lr_random_search.best_score_

print('Best Logistic Regression params:', lr_best_params)
print('Best Logistic Regression score:', lr_best_score)

# Membuat model menggunakan parameter terbaik
lr_model = LogisticRegression(**lr_best_params)

# Fitting model
lr_model.fit(X_train_preprocessed, y_train)
Best Logistic Regression params: {'C': 1, 'max_iter': 100, 'solver': 'liblinear'}
Best Logistic Regression score: 0.7658536585365854
Out[12]:
LogisticRegression(C=1, solver='liblinear')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
LogisticRegression(C=1, solver='liblinear')
In [13]:
# Logistic regression evaluation

# Accuracy score
print('Logistic Regression accuracy score:',
      lr_model.score(X_test_preprocessed, y_test))

# Confusion matrix
y_pred = lr_model.predict(X_test_preprocessed)
cm = confusion_matrix(y_test, y_pred)
cm_df = pd.DataFrame(cm, index=['False', 'True'], columns=['False', 'True'])
fig = px.imshow(cm_df, color_continuous_scale='Viridis',
                text_auto=True, title='Confusion Matrix')
fig.update_layout(title='Confusion Matrix', width=500, height=500)
fig.show()

# Classification report
print(classification_report(y_test, y_pred))
Logistic Regression accuracy score: 0.7670454545454546
              precision    recall  f1-score   support

           0       0.81      0.75      0.78       286
           1       0.73      0.79      0.76       242

    accuracy                           0.77       528
   macro avg       0.77      0.77      0.77       528
weighted avg       0.77      0.77      0.77       528

In [14]:
# Menampilkan score kombinasi parameter terbaik dan parameternya
rf_best_params = rf_random_search.best_params_
rf_best_score = rf_random_search.best_score_

print('Best Random Forest params:', rf_best_params)
print('Best Random Forest score:', rf_best_score)

# Membuat model menggunakan parameter terbaik
rf_model = RandomForestClassifier(**rf_best_params)

# Fitting model
rf_model.fit(X_train_preprocessed, y_train)
Best Random Forest params: {'max_depth': 20, 'min_samples_leaf': 1, 'min_samples_split': 2, 'n_estimators': 100}
Best Random Forest score: 0.9528455284552845
Out[14]:
RandomForestClassifier(max_depth=20)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
RandomForestClassifier(max_depth=20)
In [15]:
# Random forest evaluation

# Accuracy score
print('Random Forest Accuracy score:',
      rf_model.score(X_test_preprocessed, y_test))

# Heatmap
y_pred = rf_model.predict(X_test_preprocessed)
cm = confusion_matrix(y_test, y_pred)
cm_df = pd.DataFrame(cm, index=['False', 'True'], columns=['False', 'True'])
fig = px.imshow(cm_df, color_continuous_scale='Viridis',
                text_auto=True, title='Confusion Matrix')
fig.update_layout(title='Confusion Matrix', width=500, height=500)
fig.show()

# Classification report
print(classification_report(y_test, y_pred))
Random Forest Accuracy score: 0.9696969696969697
              precision    recall  f1-score   support

           0       0.98      0.96      0.97       286
           1       0.96      0.98      0.97       242

    accuracy                           0.97       528
   macro avg       0.97      0.97      0.97       528
weighted avg       0.97      0.97      0.97       528

In [16]:
# Get the train and test scores for logistic regression and random forest
lr_train_score = lr_model.score(X_train_preprocessed, y_train)
lr_test_score = lr_model.score(X_test_preprocessed, y_test)
rf_train_score = rf_model.score(X_train_preprocessed, y_train)
rf_test_score = rf_model.score(X_test_preprocessed, y_test)

# Create a horizontal grouped bar chart using Plotly
fig = go.Figure(data=[go.Bar(y=['Logistic Regression', 'Random Forest'],
                             x=[lr_test_score*100, rf_test_score*100],
                             text=[f'Test: {lr_test_score*100:.2f}%',
                                   f'Test: {rf_test_score*100:.2f}%'],
                             textposition='auto',
                             name='Test',
                             orientation='h',
                             width=0.3),
                      go.Bar(y=['Logistic Regression', 'Random Forest'],
                             x=[lr_train_score*100, rf_train_score*100],
                             text=[f'Train: {lr_train_score*100:.2f}%',
                                   f'Train: {rf_train_score*100:.2f}%'],
                             textposition='auto',
                             name='Train',
                             orientation='h',
                             width=0.3)])

# Update the layout
fig.update_layout(title='Model Evaluation',
                  xaxis=dict(title='Accuracy (%)'),
                  yaxis=dict(title='Model'),
                  barmode='group',
                  width=800,
                  height=400)

# Show the plot
fig.show()
  • Berdasarkan evaluasi yang dilakukan, model yang dibuat dengan menggunakan algoritma Random Forest Classifier memiliki nilai akurasi yang lebih baik dibandingkan dengan model yang dibuat dengan menggunakan algoritma logistic regression. Oleh karena itu, model yang akan digunakan adalah model yang dibuat dengan menggunakan algoritma Random Forest Classifier.
  • Kedua algortima cenderung melakukan prediksi false positive yang berarti model memprediksi karyawan akan keluar dari perusahaan padahal karyawan tersebut tidak akan keluar dari perusahaan. Hal ini dapat disebabkan karena data yang tidak seimbang (imbalanced data).
  • Akurasi testing tidak berbeda jauh dengan akurasi training. Hal ini menunjukkan bahwa model yang dibuat tidak mengindikasikan overfitting.

Untuk penjelasan lebih detail mengenai confusion matrix, recall, dan precision silahkan kunjungi link ini.

In [17]:
# Plot the feature importance
feature_importance = rf_model.feature_importances_
feature_importance = pd.DataFrame(
    feature_importance, index=feature_names, columns=['importance'])
fig = px.bar(feature_importance.sort_values(by='importance', ascending=True)[
             -10:], x='importance', y=feature_importance.sort_values(by='importance', ascending=True)[-10:].index, orientation='h')
fig.update_layout(title='Feature Importance', xaxis_title='Importance',
                  yaxis_title='Feature', width=800, height=600)
fig.show()
  • Faktor yang paling berpengaruh terhadap Attrition berdasarkan Random Forest feature importance adalah MonthlyIncome. Beradasarkan nilai korelasi, maka dapat dikatakan bahwa semakin tinggi MonthlyIncome maka semakin kecil kemungkinan karyawan tersebut akan keluar dari perusahaan.
  • Hal serupa juga terjadi pada faktor Age. Semakin tinggi usia karyawan maka semakin kecil kemungkinan karyawan tersebut akan keluar dari perusahaan.
  • Bersinggungan dengan analisis sebelumnya faktor OverTime juga memiliki pengaruh yang cukup besar terhadap Attrition.
In [18]:
# save model
dump(lr_model, 'joblib/lr_model.joblib')
dump(rf_model, 'joblib/rf_model.joblib')
Out[18]:
['joblib/rf_model.joblib']

Prediksi¶

In [19]:
df_cols = df.columns[1:]
df_predict_cols = df_predict.columns

# Cek urutan kolom df dan df_predict
for i, col in enumerate(df_cols):
    if col != df_predict_cols[i]:
        print(
            f"Column order mismatch: {col} in df is not the same as {df_predict_cols[i]} in df_predict")
        break
else:
    print("Column order is the same")
Column order is the same
In [20]:
# Memprediksi df_predict
## Preprocess df_predict
df_predict_preprocessed = preprocessor.transform(df_predict)

## Predict
y_pred = rf_model.predict(df_predict_preprocessed)

## Create a dataframe
df_predict['Attrition'] = y_pred
df_predict.head()
Out[20]:
Age BusinessTravel DailyRate Department DistanceFromHome Education EducationField EnvironmentSatisfaction Gender HourlyRate ... RelationshipSatisfaction StockOptionLevel TotalWorkingYears TrainingTimesLastYear WorkLifeBalance YearsAtCompany YearsInCurrentRole YearsSinceLastPromotion YearsWithCurrManager Attrition
0 38 Travel_Frequently 1444 Human Resources 1 4 Other 4 Male 88 ... 2 1 7 2 3 6 2 1 2 0
4 40 Travel_Rarely 1194 Research & Development 2 4 Medical 3 Female 98 ... 2 3 20 2 3 5 3 0 2 0
5 29 Travel_Rarely 352 Human Resources 6 1 Medical 4 Male 87 ... 4 0 1 3 3 1 0 0 0 0
12 47 Travel_Rarely 571 Sales 14 3 Medical 3 Female 78 ... 3 1 11 4 2 5 4 1 2 0
18 25 Travel_Frequently 772 Research & Development 2 1 Life Sciences 4 Male 77 ... 3 2 7 6 3 7 7 0 7 0

5 rows × 31 columns

In [21]:
# combine df and df_predict
df_combined = pd.concat([df, df_predict], ignore_index=True)
df_combined.info()

# save csv
df_combined.to_csv('data/final_data.csv', index=False)
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1470 entries, 0 to 1469
Data columns (total 31 columns):
 #   Column                    Non-Null Count  Dtype  
---  ------                    --------------  -----  
 0   Attrition                 1470 non-null   int32  
 1   Age                       1470 non-null   int64  
 2   BusinessTravel            1470 non-null   object 
 3   DailyRate                 1470 non-null   int64  
 4   Department                1470 non-null   object 
 5   DistanceFromHome          1470 non-null   int64  
 6   Education                 1470 non-null   int64  
 7   EducationField            1470 non-null   object 
 8   EnvironmentSatisfaction   1470 non-null   int64  
 9   Gender                    1470 non-null   object 
 10  HourlyRate                1470 non-null   int64  
 11  JobInvolvement            1470 non-null   int64  
 12  JobLevel                  1470 non-null   int64  
 13  JobRole                   1470 non-null   object 
 14  JobSatisfaction           1470 non-null   int64  
 15  MaritalStatus             1470 non-null   object 
 16  MonthlyIncome             1470 non-null   int64  
 17  MonthlyRate               1470 non-null   int64  
 18  NumCompaniesWorked        1470 non-null   int64  
 19  OverTime                  1470 non-null   object 
 20  PercentSalaryHike         1470 non-null   int64  
 21  PerformanceRating         1470 non-null   int64  
 22  RelationshipSatisfaction  1470 non-null   int64  
 23  StockOptionLevel          1470 non-null   int64  
 24  TotalWorkingYears         1470 non-null   float64
 25  TrainingTimesLastYear     1470 non-null   int64  
 26  WorkLifeBalance           1470 non-null   int64  
 27  YearsAtCompany            1470 non-null   float64
 28  YearsInCurrentRole        1470 non-null   float64
 29  YearsSinceLastPromotion   1470 non-null   float64
 30  YearsWithCurrManager      1470 non-null   float64
dtypes: float64(5), int32(1), int64(18), object(7)
memory usage: 350.4+ KB

Kesimpulan¶

  1. Dari hasil analisis yang telah dilakukan, maka dapat disimpulkan bahwa faktor yang paling berpengaruh terhadap Attrition adalah MonthlyIncome, Age, dan OverTime.
  2. Model Random Forest Classifier memiliki nilai akurasi yang sangat baik untuk melakukan prediksi pada karyawan yang akan keluar dari perusahaan. Walaupun model cenderung melakukan prediksi false positive.

Saran¶

Untuk mengurangi jumlah karyawan yang keluar dari perusahaan, maka perusahaan dapat melakukan beberapa hal berikut:

  • Memberikan gaji yang lebih tinggi kepada karyawan terutama pada karyawan muda.
  • Memberikan insentif kepada karyawan yang bekerja lembur atau meniadakan lembur.